package graph.editor;

import graph.util.AutomatonInfo;
import graph.util.Shapes;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Point2D;
import java.awt.geom.RectangularShape;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Observable;
import java.util.Observer;

import javax.swing.JComponent;
import javax.swing.JTextField;
import javax.swing.event.MouseInputListener;

import automaton.State;
import automaton.StateImpl;
import automaton.Transition;
import automaton.TransitionImpl;

import observable.ObservableAutomaton;

public class GraphComponent extends JComponent implements MouseInputListener,
		KeyListener, Observer {

	private static int n = 0;

	List<Vertex> vertices = new ArrayList<Vertex>();
	Vertex currentVertex = null;

	/* Added by Lam Ho */
	//private RectangularShape vertexSample = new Ellipse2D.Double(0, 0, 10, 10);
	public static final int SHAPE_SIZE = 30;
	private RectangularShape vertexSample = new Ellipse2D.Double(0, 0, SHAPE_SIZE, SHAPE_SIZE);
	private AutomatonInfo info = null;
	private Edge entryEdge = null;
	private List<Edge> exitEdges = new ArrayList<Edge>();
	/* End: Added by Lam Ho */
	
	private RectangularShape currentJointPoint = null;

	List<Edge> edges = new ArrayList<Edge>();
	Edge currentEdge = null;

	public GraphComponent() {
		addMouseListener(this);
		addMouseMotionListener(this);
		addKeyListener(this);
	}
	
	public AutomatonInfo getInfo() {
		return info;
	}

	public void setInfo(AutomatonInfo info) {
		this.info = info;
	}
	
	/* 
	 * This method will create the elements (vertices, edges, entryEdge, exitEdges)
	 * from the input automaton. These elements are necessary to draw this automaton
	 * on the component.
	 */
	public void convertAutomatonIntoGraph(ObservableAutomaton<String> automaton) {
		if ((automaton == null) || (info == null)) {
			return; // do nothing
		}
		List<String> states = info.getStates();
		List<String> inputs = info.getInputs();
		// Removes all of the elements from these lists
		vertices.clear();
		edges.clear();
		/*
		 * Initialize the vertices for representing the Automaton states
		 * Calculate the coordinate of all vertices on circle
		 */
		int radius = 250;
		int dx = 100, dy = 100;
		int stateNum = states.size();
		Point2D center = new Point2D.Double(dx + radius, dy + radius);
		double angle = Math.PI;
		double deltaAngle = 2 * Math.PI / stateNum;
		for (String state : states) {
			double x = center.getX() + radius * Math.cos(angle);
			double y = center.getY() - radius * Math.sin(angle);
			RectangularShape rs = newVertexShape((int) x, (int) y);
			vertices.add(new Vertex(rs, state));
			angle -= deltaAngle;
		}
		/* Initialize the edges for representing the Automaton transitions */
		Map<State, Map<String, Transition<String>>> transitions = automaton.getTransitions();
		Object[] statesArray = transitions.keySet().toArray();
		int[][] edgeNum = new int[stateNum][stateNum];
		//edgeNum[i][j] is the number of transitions between state i and state j
		for (Object state : statesArray) {
			StateImpl s1 = (StateImpl) state;
			Vertex v1 = getVertex(s1.getLabel());
			int index1 = vertices.indexOf(v1);
			
			// Process initial state for displaying
			RectangularShape shape = v1.shape;
			double vertexX = shape.getCenterX();
			double vertexY = shape.getCenterY();
			if (s1.initial()) {
				double x = (center.getY() - vertexY)/5 + vertexX;
				double y = (vertexX - center.getX())/5 + vertexY;
				Vertex start = new Vertex(newVertexShape((int) x, (int) y), null);
				entryEdge = new Edge(start, v1, null, 0, Edge.ENTRY_EDGE);
			}
			
			// Process final states for displaying
			if (s1.terminal()) {
				double x = (vertexY - center.getY())/5 + vertexX;
				double y = (center.getX() - vertexX)/5 + vertexY;
				Vertex end = new Vertex(newVertexShape((int) x, (int) y), null);
				Edge edge = new Edge(v1, end, null, 0, Edge.EXIT_EDGE);
				exitEdges.add(edge);
			}
			
			// Create new edge for displaying each transition
			for (String input : inputs) {
				Transition<String> t = automaton.transition(s1, input);
				if (t != null) {
					Vertex v2 = getVertex(((StateImpl) t.target()).getLabel());
					int index2 = vertices.indexOf(v2);
					edgeNum[index1][index2]++;
					edgeNum[index2][index1] = edgeNum[index1][index2];
					Edge edge = new Edge(v1, v2, input, edgeNum[index1][index2], Edge.NORMAL_EDGE);
					if (index1 == index2) {
						// This transition has the same source and target
						int r = 50;
						double x = (vertexX + vertexX * radius / r - center.getX()) * r / radius;
						double y = (vertexY + vertexY * radius / r - center.getY()) * r / radius;
						Point2D center1 = new Point2D.Double(x, y);
						List<Point2D> jointPoints = new ArrayList<Point2D>();
						int numberOfJointPoints = 7;
						angle = 0;
						deltaAngle = 2 * Math.PI / (numberOfJointPoints + 1);
						for (int k = 0; k < numberOfJointPoints + 1; k++) {
							double jointPointX = center1.getX() + r * Math.cos(angle);
							double jointPointY = center1.getY() - r * Math.sin(angle);
							jointPoints.add(new Point2D.Double(jointPointX, jointPointY));
							angle += deltaAngle;
						}
						int quarter = 0;
						if (vertexX < x) {
							if (vertexY < y) {
								quarter = 2;
							} else {
								quarter = 3;
							}
						} else {
							if (vertexY < y) {
								quarter = 1;
							} else {
								quarter = 4;
							}
						}
						int start1 = (quarter - 1) * 2;
						double d1 = Point2D.distance(vertexX, vertexY,
								jointPoints.get(start1).getX(), jointPoints.get(start1).getY());
						int start2 = (start1 + 2) % 8;
						double d2 = Point2D.distance(vertexX, vertexY,
								jointPoints.get(start2).getX(), jointPoints.get(start2).getY());
						if (d1 < d2) {
							int tmp = start1;
							for (int i1 = 0; i1 < numberOfJointPoints; i1++) {
								edge.addJointPoint(jointPoints.get(tmp));
								tmp = (tmp == 0? jointPoints.size() - 1: tmp - 1);
							}
						} else {
							int tmp = start2;
							for (int i1 = 0; i1 < numberOfJointPoints; i1++) {
								edge.addJointPoint(jointPoints.get(tmp));
								tmp = (tmp == jointPoints.size() - 1? 0: tmp + 1);
							}
						}
					}
					edges.add(edge);
				}
			}
		}
		repaint();
	}
	
	/*
	 * This method will return one vertex from vertices list if it can find
	 * any vertex having the same label as the input label.
	 * Otherwise, return null.
	 */
	private Vertex getVertex(String label) {
		for (Vertex v : vertices) {
			if (v.label.equals(label)) {
				return v;
			}
		}
		return null;
	}

	/*
	 * This method will return one edge from edges list if it can find
	 * any edge having the same source label and same target label
	 * as the input labels. Otherwise, return null.
	 */
	private Edge getEdge(String label1, String label2) {
		for (Edge e : edges) {
			if (e.v1.label.equals(label1) && e.v2.label.equals(label2)) {
				return e;
			}
		}
		return null;
	}

	/*
	 * This method will return the entryEdge if the entryEdge has
	 * the same target label as the input label. Otherwise, return null.
	 */
	private Edge getEntryEdge(String label) {
		if (entryEdge.v2.label.equals(label)) {
			return entryEdge;
		}
		return null;
	}

	/*
	 * This method will return one edge from exitEdges list if it can find
	 * any edge having the same source label as the input labels.
	 * Otherwise, return null.
	 */
	private Edge getExitEdge(String label) {
		for (Edge e : exitEdges) {
			if (e.v1.label.equals(label)) {
				return e;
			}
		}
		return null;
	}

	protected void paintComponent(Graphics g) {
		if (isOpaque()) {
			g.setColor(getBackground());
			g.fillRect(0, 0, getWidth(), getHeight());
		}
		g.setColor(getForeground());
		Graphics2D g2 = (Graphics2D) g;
		/* Modified by Lam Ho */
		RenderingHints rh = new RenderingHints(
				RenderingHints.KEY_ANTIALIASING, 
				RenderingHints.VALUE_ANTIALIAS_ON);
		g2.setRenderingHints(rh);
		// Draw the entry edge
		if (entryEdge != null) {
			entryEdge.draw(g2);
		}
		// Draw the exit edges
		for (Edge e : exitEdges) {
			e.draw(g2);
		}
		for (Edge e : edges)
			e.draw(g2);
		for (Vertex v : vertices) {
			//v.draw(g2);
			// Replaced by this method to draw an Automaton state
			v.drawState(g2);
		}
		/* End: Modified by Lam Ho */
	}

	private Vertex getVertex(int x, int y) {
		for (Vertex v : vertices) {
			if (v.shape.contains(x, y))
				return v;
		}
		return null;
	}

	private RectangularShape getJointPoint(int x, int y) {
		for (Edge e : edges) {
			RectangularShape jp = e.getJointPoint(x, y);
			if (jp != null)
				return jp;
		}
		return null;
	}

	private static final double EDGE_EPSILON = 2.0;

	private Edge getEdge(int x, int y) {
		for (Edge e : edges)
			if (e.contains(x, y, EDGE_EPSILON))
				return e;
		return null;
	}

	public void setVertexSample(RectangularShape sample) {
		vertexSample = sample;
	}

	private RectangularShape newVertexShape(int x, int y) {
		RectangularShape rs = (RectangularShape) vertexSample.clone();
		Shapes.moveShape(rs, x, y);
		return rs;
	}

	private Vertex createVertex(int x, int y) {
		RectangularShape rs = newVertexShape(x, y);
		Vertex v = new Vertex(rs, Integer.toString(n++));
		vertices.add(v);
		return v;
	}

	private void removeVertex(Vertex v) {
		List<Edge> toRemove = new ArrayList<Edge>();
		for (Edge e : edges) {
			if (e.v1 == v || e.v2 == v)
				toRemove.add(e);
		}
		for (Edge e : toRemove)
			removeEdge(e);
		vertices.remove(v);
	}

	private void removeEdge(Edge e) {
		remove(e.textField);
		edges.remove(e);
	}

	private Edge startEdge(Vertex v) {
		RectangularShape rs2 = newVertexShape(0, 0);
		RectangularShape rs = v.shape;
		rs2.setFrameFromCenter((int) rs.getCenterX(), (int) rs.getCenterY(),
				(int) rs.getCenterX(), (int) rs.getCenterY());
		Edge l = new Edge(v, new Vertex(rs2, null));
		edges.add(l);
		return l;
	}

	private void addJointPoint(Edge e) {
		e.addJointPoint();
	}

	private void endLine(Edge e, int x, int y) {
		Vertex v = getVertex(x, y);
		if (v == null) {
			e.v2.shape.setFrameFromCenter(x, y, x + vertexSample.getHeight()
					/ 2, y + vertexSample.getWidth() / 2);
			e.v2.label = Integer.toString(n++);
			vertices.add(e.v2);
		} else
			e.v2 = v;
	}

	public void mousePressed(MouseEvent e) {
		requestFocusInWindow();
		if ((e.getModifiersEx() & InputEvent.BUTTON3_DOWN_MASK) == InputEvent.BUTTON3_DOWN_MASK)
			return;
		int x = e.getX();
		int y = e.getY();
		System.out.println("x = " + x + ", y = " + y);
		Vertex v = getVertex(x, y);
		if (v == null) {
			currentJointPoint = getJointPoint(x, y);
		}
		if (v == null && currentJointPoint == null)
			v = createVertex(x, y);
		if (v != null && e.isAltDown())
			currentEdge = startEdge(v);
		else
			currentVertex = v;
		repaint();
	}

	public void mouseDragged(MouseEvent e) {
		if (currentVertex != null) {
			Shapes.moveShape(currentVertex.shape, e.getX(), e.getY());
			repaint();
		} else if (currentEdge != null) {
			Shapes.moveShape(currentEdge.v2.shape, e.getX(), e.getY());
			repaint();
		} else if (currentJointPoint != null) {
			Shapes.moveShape(currentJointPoint, e.getX(), e.getY());
			repaint();
		}
	}

	public void addEdgeLabel(final Edge e) {
		final JTextField textField = new JTextField();
		textField.setSize(100, 20);
		textField.setLocation(e.labelPosition());
		e.textField = textField;
		this.add(textField);
	}

	public void mouseReleased(MouseEvent e) {
		if (currentEdge != null) {
			endLine(currentEdge, e.getX(), e.getY());
			addEdgeLabel(currentEdge);
			currentEdge = null;
			repaint();
		}
		currentVertex = null;
		currentJointPoint = null;
	}

	public void mouseMoved(MouseEvent e) {
	}

	public void mouseEntered(MouseEvent e) {
	}

	public void mouseExited(MouseEvent e) {
	}

	public void mouseClicked(MouseEvent e) {
		if (e.getButton() == MouseEvent.BUTTON3) {
			int x = e.getX();
			int y = e.getY();
			Vertex v = getVertex(x, y);
			if (v != null) {
				removeVertex(v);
				repaint();
				return;
			}
			for (Edge edge : edges) {
				RectangularShape jp = edge.getJointPoint(x, y);
				if (jp != null) {
					edge.removeJointPoint(jp);
					repaint();
					return;
				}
			}
			Edge edge = getEdge(x, y);
			if (edge != null) {
				removeEdge(edge);
				repaint();
				return;
			}
		}
	}

	public void keyPressed(KeyEvent e) {
		if (e.getKeyCode() == ' ' && currentEdge != null) {
			addJointPoint(currentEdge);
		}
	}

	public void keyReleased(KeyEvent e) {
	}

	public void keyTyped(KeyEvent e) {
	}

	/*
	 * This method will be called automatically whenever an automaton
	 * changes its current state. To do so, the programmer must register
	 * this component as the observer of the automaton by calling
	 * addObserver(GraphComponent component) on this automaton.
	 */
	@Override
	public void update(Observable o, Object arg) {
		processVisualization((Transition<String>) arg);
	}
	
	/*
	 * This method will cause a delay of i microsecond.
	 */
	private void delay(int i) {
		try {
			Thread.sleep(i);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	/*
	 * This method is used to visualize the state change on the component.
	 * At the beginning, it will draw the entry edge if this transition is
	 * the first transition in the acknowledgment. For each transition,
	 * it will do as follows:
	 * 		- draw the source state and delay an amount of time
	 * 		- draw the edge connecting the source state and the target
	 * 			state and delay an amount of time
	 * 		- draw the target state and delay an amount of time
	 * At last, if this transition is the last transition in the acknowledgment
	 * it will draw the corresponding exit edge 
	 */
	private void processVisualization(Transition<String> t) {
		if (t == null) {
			return;
		}
		Graphics2D g2 = (Graphics2D) getGraphics();
		String label1 = ((StateImpl) t.source()).getLabel();
		String label2 = ((StateImpl) t.target()).getLabel();
		g2.setColor(Color.RED);
		if (((TransitionImpl<String>) t).isStarted() && t.source().initial()) {
			getEntryEdge(label1).draw(g2);
		}
		getVertex(label1).drawState(g2);
		delay(1000); // This delay allows user to observe the on-screen result
		
		g2.setColor(Color.GREEN);
		getEdge(label1, label2).draw(g2);
		delay(1000); // This delay allows user to observe the on-screen result
		
		g2.setColor(Color.BLUE);
		getVertex(label2).drawState(g2);
		delay(1000); // This delay allows user to observe the on-screen result
		if (((TransitionImpl<String>) t).isTerminated() && t.target().terminal()) {
			getExitEdge(label2).draw(g2);
		}
		// Change to original color
		g2.setColor(getForeground());
	}
}
